/*
 * Created on Aug 9, 2007
 *
 * Copyright (c) 2007, Jonathan Fuerth
 * 
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in
 *       the documentation and/or other materials provided with the
 *       distribution.
 *     * Neither the name of Jonathan Fuerth nor the names of other
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package net.bluecow.version;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * The UpdateFinder retrieves and parses information about updates
 * to the game code itself as well as checking for new level packs
 * available via the official channels.
 *
 * @version $Id:$
 */
public class UpdateFinder {

    /**
     * Controls whether or not the debugging features of this class are enabled.
     */
    private static final boolean debugOn = false;
    
    /**
     * Prints the given printf-formatted message, followed by a newline,
     * to the console if debugOn == true.
     */
    private void debugf(String fmt, Object ... args) {
        if (debugOn) debug(String.format(fmt, args));
    }

    /**
     * Prints the given string followed by a newline to the console if debugOn==true.
     */
    private void debug(String msg) {
        if (debugOn) System.out.println(msg);
    }
    
    /**
     * The UpdateDocHandler processes SAX events generated by parsing a
     * product version description XML document.
     */
    public class UpdateDocHandler extends DefaultHandler {

        private Map<String, Product> products = new LinkedHashMap<String, Product>();
        
        private Product currentProduct;

        private ProductVersion currentVersion;
        
        private Stack<String> docPath = new Stack<String>();
        
        private StringBuilder text;
        
        private DateFormat dateFmt = new SimpleDateFormat("yyyyMMdd'T'hhmmss");
        
        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
            docPath.push(qName);
            try {
                if (qName.equals("product")) {
                    currentProduct = new Product();
                    products.put(attributes.getValue("id"), currentProduct);
                } else if (qName.equals("product-name")) {
                    text = new StringBuilder();
                } else if (qName.equals("product-description")) {
                    text = new StringBuilder();
                } else if (qName.equals("version")) {
                    currentVersion = new ProductVersion();
                    currentVersion.setVersion(new Version(attributes.getValue("ver")));
                    currentVersion.setDownloadUri(new URI(attributes.getValue("href")));
                    currentVersion.setReleaseDate(dateFmt.parse(attributes.getValue("date")));
                    currentProduct.addVersion(currentVersion);
                } else if (qName.equals("version-description")) {
                    text = new StringBuilder();
                } else if (qName.equals("depend")) {
                    final String refid = attributes.getValue("refid");
                    final Version v = new Version(attributes.getValue("version"));
                    if (v == null) {
                        throw new SAXException("Element <depend> is missing mandatory attribute \"version\"");
                    }
                    Product p = products.get(refid);
                    if (p == null) {
                        throw new SAXException("Invalid product reference id: \""+refid+"\"");
                    }
                    ProductVersion pv = p.getVersion(v);
                    if (pv == null) {
                        throw new SAXException("Invalid version dependency: " +
                                "There is no version "+v+" of product id \""+refid+"\". " +
                                "Its versions are: " + p.getVersions());
                    }
                    currentVersion.addDependency(pv);
                } else {
                    debugf("startElement: Ignoring unknown element <%s>", qName);
                }
            } catch (Exception ex) {
                if (ex instanceof SAXException) {
                    SAXException sex = (SAXException) ex;
                    throw sex;
                } else {
                    throw new SAXException(ex);
                }
            }
        }
        
        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException {
            if (qName.equals("product-name")) {
                currentProduct.setName(text.toString());
                text = null;
            } else if (qName.equals("product-description")) {
                currentProduct.setDescription(text.toString());
                text = null;
            } else if (qName.equals("version-description")) {
                currentVersion.setDescription(text.toString());
                text = null;
            }

            docPath.pop();
        }
        
        @Override
        public void characters(char[] ch, int start, int length) throws SAXException {
            if (text != null) {
                text.append(ch, start, length);
            }
        }
    }

    /**
     * The URI to check for the update XML document.
     */
    private final URI updateUri;
    
    public UpdateFinder(URI updateUri) throws URISyntaxException {
        this.updateUri = updateUri;
    }
    
    /**
     * Returns the list of all products and versions described by the XML
     * document at the update URL.
     */
    public List<Product> findAllProducts() throws IOException, SAXException {
        URL updateUrl = updateUri.toURL();
        InputStream inStream = new BufferedInputStream(updateUrl.openStream());
        UpdateDocHandler handler = new UpdateDocHandler();
        try {
            SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
            parser.parse(inStream, handler);
        } catch (ParserConfigurationException ex) {
            IOException ioe = new IOException("Couldn't create XML level parser");
            ioe.initCause(ex);
            throw ioe;
        }
        
        return new ArrayList<Product>(handler.products.values());
    }
    
    public static void main(String[] args) {
        try {
            UpdateFinder uf = new UpdateFinder(
                    new URI("file:///Users/fuerth/prg/workspace/grodbots/doc/example-updates.xml"));
            System.out.println(uf.findAllProducts());
        } catch (SAXException ex) {
            ex.printStackTrace();
            if (ex.getException() != null) {
                System.err.println("Caused by:");
                ex.getException().printStackTrace();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}
